import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.*
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory

/**
 * @author: TomHawk
 * @description:自定义任务用于生成 ViewBinding
 * @date: 2025/8/6 16:34
 */
// 自定义任务用于生成 ViewBinding
abstract class FxmlViewBindingTask : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val fxmlFiles: ConfigurableFileCollection

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val resourcesDir: DirectoryProperty

    @TaskAction
    fun generateBindings() {
        val outputDirFile = outputDir.get().asFile
        outputDirFile.mkdirs()

        fxmlFiles.forEach { fxmlFile ->
            val className = fxmlFile.nameWithoutExtension
                .split("_", "-")
                .joinToString("") { it.replaceFirstChar { char -> char.uppercase() } } + "Binding"
            val packageName = detectPackageName(fxmlFile)
            val outputFile = File(outputDirFile, "${packageName.replace('.', '/')}/$className.java")

            if (needsRegeneration(fxmlFile, outputFile)) {
                generateBindingClass(fxmlFile, className, packageName, outputFile)
            }
        }
    }

    private fun detectPackageName(fxmlFile: File): String {
        val resourcesDirFile = resourcesDir.get().asFile
        val relativePath = resourcesDirFile.toPath().relativize(fxmlFile.toPath()).toString()
        val resPkgName = relativePath.substringBeforeLast(File.separator).replace(File.separator[0], '.')
        //去掉 resources 后的部分作为包名，如果 fxml 放在 resources 根目录下则包名为空：
        return if (!resPkgName.contains(".")) "" else resPkgName.substringAfter("resources.")
    }

    private fun needsRegeneration(inputFile: File, outputFile: File): Boolean {
        return !outputFile.exists() ||
                inputFile.lastModified() > outputFile.lastModified()
    }

    private fun generateBindingClass(fxmlFile: File, className: String, packageName: String, outputFile: File) {
        val doc: Document = DocumentBuilderFactory.newInstance()
            .newDocumentBuilder()
            .parse(fxmlFile)

        //如果在布局中定义了 fx:controller 则不生成 ViewBinding：
        if (doc.documentElement.hasAttribute("fx:controller")) {
            logger.lifecycle("Ignore ViewBinding because controller is defined in fxml")
            return
        }

        //收集 import 内容：
        val imports = mutableListOf<String>()
        for (i in 0 until doc.childNodes.length) {
            val childNode = doc.childNodes.item(i)
            if (childNode.nodeName == "import") imports.add(childNode.nodeValue)
            else break
        }

        //收集 fx:id 控件：
        val elementsWithId = mutableListOf<Pair<String, String>>()
        traverse(elementsWithId, doc.documentElement)

        //控件引入：
        val viewImports = imports.joinToString("") { "import ${it};\n" }
        //控件注解拼接：
        val viewAnnotations =
            if (elementsWithId.isEmpty()) ""
            else elementsWithId.joinToString("\n") { (type, id) -> "    @FXML\n    public $type $id;" } + "\n\n"

        val template = """
// Auto-generated from ${fxmlFile.name}
${if (packageName.isEmpty()) "" else "package ${packageName};"}

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
$viewImports
import java.io.IOException;

public final class $className {
    public Parent root;
    
    private ${className}() {
    }   
        
$viewAnnotations    public static $className bind() throws IOException {
        return bind("${fxmlFile.name}");
    }

    public static $className bind(String layoutPath) throws IOException {
        $className binding = new ${className}();
        FXMLLoader loader = new FXMLLoader(binding.getClass().getResource(layoutPath));
        loader.setController(binding);
        binding.root = loader.load();
        return binding;
    }
}
        """.trimIndent()

        outputFile.parentFile.mkdirs()
        outputFile.writeText(template)
        logger.lifecycle("Generated ViewBinding: ${outputFile.absolutePath}")
    }

    // 递归遍历所有节点
    private fun traverse(elementsWithId: MutableList<Pair<String, String>>, element: Element) {
        if (element.hasAttribute("fx:id")) {
            val type = element.tagName
            val id = element.getAttribute("fx:id")
            elementsWithId.add(type to id)
        }
        val children = element.childNodes
        for (i in 0 until children.length) {
            val node = children.item(i)
            if (node is Element) {
                traverse(elementsWithId, node)
            }
        }
    }
}